﻿using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Configuration;
using System.DirectoryServices.AccountManagement;

namespace QJY.API
{
    public class LDAPHelper
    {
        private string searchProtocol="LDAP";

        private string gcHostname = ConfigurationManager.AppSettings["domain"];

        /// <summary>
        /// Initialize new instance of this active directory context.
        /// </summary>
        /// <param name="gcHostname">The Global Catalog Hostname a Domain Controller.
        /// If no hostname is provided, the current context is used.
        /// <param name="LDAPSearchType">Whether the searches will be done as GC (default) or LDAP. 
        /// Global Catalog will ensure that the results are searched for in the entire catalog (whether in the host 
        /// or other domains in the forest)
        /// But, if the host controller happens to contains the search entities, LDAP can be faster.</param>
        /// </param>
        public LDAPHelper()
        {
        }

        /// <summary>
        /// Get user by domain (exmpl.wdc.com) and SAM Account Name (last_f)
        /// </summary>
        /// <param name="domain">Domain (exmpl.wdc.com)</param>
        /// <param name="samAccountName">SAM Account Name (last_f)</param>
        public LDAPUser GetUser(string samAccountName)
        {
            if (string.IsNullOrEmpty(gcHostname))
            {
                throw new ArgumentException("Invalid argument: domain is null or empty", "domain");
            }

            if (string.IsNullOrEmpty(samAccountName))
            {
                throw new ArgumentException("Invalid argument: samAccountName is null or empty", "samAccountName");
            }

            string branch = ConvertDomainToDistinguishedNameFormat(gcHostname);
            string path = GetDirectoryPath(branch);
            string filter = string.Format("(&(objectClass=person)(samAccountName={0}))", samAccountName);

            using (var entry = new DirectoryEntry(path))
            using (var search = new DirectorySearcher(entry, filter))
            {
                SearchResult result = search.FindOne();
                if (result == null)
                {
                    return null;
                }
                else
                {
                    return GetUserFromResult(result);
                }
            }
        }




        
        /// <summary>
        /// Perform authentication and return the status for the account.
        /// </summary>
        /// <param name="user">The user account object</param>
        /// <param name="password">The user password in plaintext</param>
        /// <param name="daysToExpiration">The number of days until the password expires</param>
        public LDAPUserStatus Authenticate(LDAPUser user, string password, long daysToExpiration = 0)
        {
            LDAPUserStatus status = LDAPUserStatus.UserNotFound;

            if (user != null)
            {
                using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, user.Domain))
                using (UserPrincipal u = UserPrincipal.FindByIdentity(pc, IdentityType.DistinguishedName, user.DistinguishedName))
                {
                    if (u == null)
                    {
                        status = LDAPUserStatus.UserNotFound;
                    }
                    else if (u.PasswordNotRequired)
                    {
                        status = LDAPUserStatus.Success;
                    }
                    else if (pc.ValidateCredentials(user.SamAccountName, password))
                    {
                        status = LDAPUserStatus.Success;
                    }
                    else if (u.IsAccountLockedOut())
                    {
                        status = LDAPUserStatus.UserLockedOut;
                    }
                    else if (!u.PasswordNeverExpires && u.LastPasswordSet == null)
                    {
                        status = LDAPUserStatus.MustChangePassword;
                    }
                    else
                    {
                        if (daysToExpiration == 0)
                        {
                            // If the caller does not provide expiration, use the domain policy
                            daysToExpiration = GetMaxPasswordAgeInDays(user.Domain);
                        }

                        if (!u.PasswordNeverExpires && (DateTime.Now - u.LastPasswordSet.Value).TotalDays >= daysToExpiration)
                        {
                            status = LDAPUserStatus.ExpiredPassword;
                        }
                        else
                        {
                            status = LDAPUserStatus.InvalidPassword;
                        }
                    }
                }
            }

            return status;
        }
        
        
        /// <summary>
        /// Gets the maxPwdAge property on the domain (exmpl.wdc.com).
        /// This is days until the password expires unless the account has PasswordNeverExpires.
        /// </summary>
        /// <param name="domain">Domain Name (exmpl.wdc.com)</param>
        /// <returns>Days until a password expires</returns>
        public long GetMaxPasswordAgeInDays(string domain)
        {
            long days = 0;
            const long NS_IN_A_DAY = -864000000000;

            string baseDistinguishedName = ConvertDomainToDistinguishedNameFormat(domain);
            string path = string.Format("LDAP://{0}/{1}", gcHostname, baseDistinguishedName);
            string filter = "(maxPwdAge=*)";

            using (var entry = new DirectoryEntry(path))
            using (var search = new DirectorySearcher(entry, filter, new string[] { "+", "*" }, SearchScope.Base))
            {
                var result = search.FindOne();
                if (result != null && result.Properties.Contains("maxPwdAge"))
                {
                    long maxPwdAge = TryGetResult<long>(result, "maxPwdAge");

                    days = maxPwdAge / NS_IN_A_DAY;
                }
            }

            return days;
        }

        /// <summary>
        /// Returns UserAccount object from a given search result
        /// </summary>
        /// <param name="result">SearchResult computed by one of the other GetUser methods</param>
        private LDAPUser GetUserFromResult(SearchResult result)
        {
            // Values can be found here:
            // http://msdn.microsoft.com/en-us/library/ms679021(v=vs.85).aspx
            return new LDAPUser()
            {
                ObjectGuid = TryGetResult<byte[]>(result, "objectGUID"),
                CommonName = TryGetResult<string>(result, "cn"),
                DisplayName = TryGetResult<string>(result, "displayName"),
                GivenName = TryGetResult<string>(result, "givenName"),
                SurName = TryGetResult<string>(result, "sn"),
                Email = TryGetResult<string>(result, "mail"),
                Department = TryGetResult<string>(result, "department"),
                Title = TryGetResult<string>(result, "title"),
                LocalityName = TryGetResult<string>(result, "l"),
                StateOrProvinceName = TryGetResult<string>(result, "st"),
                CountryName = TryGetResult<string>(result, "c"),
                Phone = TryGetResult<string>(result, "telephoneNumber"),
                Mobile = TryGetResult<string>(result, "mobile"),
                PhysicalDeliveryOfficeName = TryGetResult<string>(result, "physicalDeliveryOfficeName"),
                Description = TryGetResult<string>(result, "description"),
                JpegPhoto = TryGetResult<byte[]>(result, "thumbnailPhoto"),
                DistinguishedName = TryGetResult<string>(result, "distinguishedName"),
                Domain = GetDomainFromDistinguishedName(TryGetResult<string>(result, "distinguishedName")),
                SamAccountName = TryGetResult<string>(result, "samAccountName"),
                UserPrincipalName = TryGetResult<string>(result, "userPrincipalName"),
                Manager = TryGetResult<string>(result, "manager"),
                Company = TryGetResult<string>(result, "company"),
                DirectReports = TryGetResultList<string>(result, "directReports")
            };
        }

        private T TryGetResult<T>(SearchResult result, string key)
        {
            var valueCollection = result.Properties[key];
            if (valueCollection.Count > 0)
                return (T)valueCollection[0];
            else
                return default(T);
        }

        private List<T> TryGetResultList<T>(SearchResult result, string key)
        {
            var list = new List<T>();
            var valueCollection = result.Properties[key];
            if (valueCollection.Count > 0)
            {
                foreach (T val in valueCollection)
                {
                    list.Add(val);
                }
            }
            return list;
        }

        /// <summary>
        /// Extracts the domain component (exmpl.wdc.com) from distinguished name (...DC=exmpl,DC=wdc,DC=com)
        /// </summary>
        /// <param name="distinguishedName">Distinguished Name</param>
        private string GetDomainFromDistinguishedName(string distinguishedName)
        {
            if (distinguishedName == null)
            {
                return null;
            }

            // The assumption is that the domain can be parsed from the distinguished name beginning
            // with the first DC object.
            int domainBegin = distinguishedName.IndexOf("DC=");
            if (domainBegin > -1)
            {
                return ConvertDistinguishedNameToDomainFormat(distinguishedName.Substring(domainBegin));
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Helper function that provides the directory path string to pass onto DirectoryEntry()
        /// constructor.
        /// </summary>
        /// <param name="baseDistinguishedName">Optional distinguished name of domain to search under.</param>
        /// <returns></returns>
        private string GetDirectoryPath(string baseDistinguishedName = null)
        {
            string path;

            if (!string.IsNullOrEmpty(baseDistinguishedName))
            {
                path = string.Format("{0}://{1}/{2}", searchProtocol, gcHostname, baseDistinguishedName);
            }
            else
            {
                path = string.Format("{0}://{1}", searchProtocol, gcHostname);
            }

            return path;
        }

        /// <summary>
        /// Convert (example.wdc.com) -> DC=example,DC=wdc,DC=com
        /// </summary>
        /// <param name="domain"></param>
        /// <returns></returns>
        private string ConvertDomainToDistinguishedNameFormat(string domain)
        {
            return "DC=" + domain.Replace(".", ",DC=");
        }

        /// <summary>
        /// Convert (DC=example,DC=wdc,DC=com) -> example.wdc.com
        /// </summary>
        /// <param name="distinguishedName"></param>
        /// <returns></returns>
        private string ConvertDistinguishedNameToDomainFormat(string distinguishedName)
        {
            return distinguishedName.Replace("DC=", string.Empty).Replace(",", ".");
        }

    }
}